package com.ygu.websocket.srevice.impl;

import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ygu.user.WxMsgService;
import com.ygu.user.dao.UserDao;
import com.ygu.user.service.adapter.WSAdapter;
import com.ygu.websocket.domain.dto.WSChannelExtraDTO;
import com.ygu.websocket.domain.entity.User;
import com.ygu.websocket.domain.enums.WSBaseResp;
import com.ygu.websocket.srevice.LoginService;
import com.ygu.websocket.srevice.WebSocketService;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Service
public class WebSocketServiceImpl implements WebSocketService {

    private static final Duration EXPIRE_TIME = Duration.ofHours(1);
    private static final Long MAX_MUM_SIZE = 10000L;

    //对于用户第一次连接的时候 要我们需要将该用户的连接通道进行保存便于后续的保存使用
    private static final ConcurrentHashMap<Channel, WSChannelExtraDTO> ONLINE_MAP = new ConcurrentHashMap<>();

    //这个service包含了微信所有的API
    @Resource
    @Lazy
    private WxMpService wxMpService;
    @Resource
    @Lazy
    private WxMsgService wxMsgService;
    @Resource
    private UserDao userDao;
    @Resource
    private LoginService loginService;

    /**
     * 所有请求登录的code与channel关系
     * 使用咖啡因(有过期策略 )的是 为需要进行建立登入的用户的channel
     * 防止出现oom
     */
    public static final Cache<Integer, Channel> WAIT_LOGIN_MAP = Caffeine.newBuilder()
            .expireAfterWrite(EXPIRE_TIME)
            .maximumSize(MAX_MUM_SIZE)
            .build();

    /**
     * 不论是游客还是用户 第一次连接进行来将他的Channel存储在MAP里面
     * 方便后续的使用
     * @param channel 通道
     */
    @Override
    public void connect(Channel channel) {
        ONLINE_MAP.put(channel,new WSChannelExtraDTO());
    }

    /**
     * 登入的Handle
     * @param channel 对应用户通道
     */
    @Override
    public void handleLogin(Channel channel) {
        //1.生成一个不重复的登录码。并且将登录码和这个Channel关联起来
        Integer code = generateLoginCode(channel);
        //2.然后请求微信的接口，将这个登录码转成一个带参数的二维码链接
        WxMpQrCodeTicket wxMpQrCodeTicket = null;
        try {
            wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, (int) EXPIRE_TIME.getSeconds());
        } catch (WxErrorException e) {
            log.warn("请求二维码出错啦:{}",e);
            throw new RuntimeException(e);
        }
        //3.返回给前端，二维码链接转成二维码图片展示  接下来就是用户看到二维码之后 进行扫码的操作
        sendMessages(channel, WSAdapter.builderLoginResp(wxMpQrCodeTicket));
    }

    /**
     * 用户下线操作
     * @param channel 用户下线也需要吧channel进行移除
     */
    @Override
    public void userOffLine(Channel channel) {
        ONLINE_MAP.remove(channel);
        //用户下线进行的操作
    }

    /**
     * 用户扫码登入逻辑
     * @param id 用户id
     * @param code 带参二维码的code
     */
    @Override
    public void scanLoginUser(Long id, String code) {
        //要进行登入操作 则需要进行找到对应用户的channel
        Channel channel = WAIT_LOGIN_MAP.getIfPresent(Integer.valueOf(code));
        if(Objects.isNull(channel)){
            return;
        }
        //如果存在则找到该用户进行登入
        User user = userDao.getUserById(id);
        //调用登入模块 获取token
        String token = loginService.login(id);
        //用户登入成功之后 需要返回信息给前端
        sendMessages(channel,WSAdapter.buildResp(user,token));

    }

    @Override
    public void waitAuthorize(String code) {
        Channel channel = WAIT_LOGIN_MAP.getIfPresent(Integer.valueOf(code));
        if(Objects.isNull(channel)){
            return;
        }
        sendMessages(channel,WSAdapter.buildAuthorizeResp());
    }


    /**
     * 将生成二维码的链接 通过 channel(WebSocket)返回给前端
     * 注意要使用websocket 的特定的文本传输格式
     * @param channel 通道
     * @param wsBaseResp 生成二维码的信息
     */
    private void sendMessages(Channel channel, WSBaseResp<?> wsBaseResp) {
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(wsBaseResp)));
    }

    /**
     * 生成随机CODE
     * 这边要注意的问题1.CODE不可以重复 2.在生成code的之前 要先判断 是否已经存在MAP中
     * 若存在的话 则不进行put操作
     * 避免多次生成code
     * @param channel 和code关联的通道
     * @return code
     */
    private Integer generateLoginCode(Channel channel) {

        int code ;
        do{
            code = RandomUtil.randomInt(Integer.MAX_VALUE);
        }while (Objects.nonNull(WAIT_LOGIN_MAP.asMap().putIfAbsent(code,channel)));

        return code;
    }
}
